﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit;
using NSubstitute;
using System.Reflection;
using System.Threading.Tasks;

namespace MetaSharp.Transformation.Tests.Patterns
{
    public class MetadataTests
    {
        [Fact]
        public async Task CanGetMetadata()
        {
            var m = await Pattern.Match(R1, "a");
            var md = m.End.Memos.GetMetadata(m.Value);
            var a = md.Single();
            var att = a.Metadata.Single();
            Assert.IsType<TestMetadata>(att);
        }

        [Fact]
        public async Task GetsMetadataOnceForRuleMultipleTimes()
        {
            var p = Pattern.Transform(R1, R1);
            var m = await Pattern.Match(p, "a");
            var md = m.End.Memos.GetMetadata(m.Value);
            Assert.IsType<TestMetadata>(md.ElementAt(0).Metadata.Single());
            Assert.IsType<TestMetadata>(md.ElementAt(1).Metadata.Single());
        }

        [Fact]
        public async Task CanGetMetadataAcrossMultipleSteps()
        {
            var p = Pattern.Transform(R1, R4);
            var m = await Pattern.Match(p, "a");
            var md = m.End.Memos.GetMetadata(m.Value);
            Assert.IsType<TestMetadata4>(md.ElementAt(0).Metadata.Single());
            Assert.IsType<TestMetadata>(md.ElementAt(1).Metadata.Single());
        }

        [Fact]
        public async Task GetsOnlyCorrectMetadata()
        {
            var p = Pattern.And(R1, R2, R3);
            var m = await Pattern.Match(p, "abc");
            var v = (object[])m.Value;
            var md1 = m.End.Memos.GetMetadata(v[0]).Single();
            var md2 = m.End.Memos.GetMetadata(v[1]).Single();
            var md3 = m.End.Memos.GetMetadata(v[2]).Single();

            Assert.IsType<TestMetadata>(md1.Metadata.Single());
            Assert.IsType<TestMetadata2>(md2.Metadata.Single());
            Assert.IsType<TestMetadata3>(md3.Metadata.Single());
        }

        [Fact]
        public async Task GetsAllMetadataFromProduction()
        {
            var p = Pattern.Rule(
                Pattern.Production(
                    Pattern.And(
                        R1,
                        Pattern.Variable("r", R2),
                        R3),
                    (object r) => new { R = r }));
            var m = await Pattern.Match(p, "abc");

            Assert.True(m.Matched);
            dynamic value = m.Value;
            var metadata = m.End.Memos.GetMetadata(value.R);

            Assert.Equal(1, metadata.Length);
            Assert.Equal(1, metadata[0].Metadata.Length);
            Assert.IsType<TestMetadata2>(metadata[0].Metadata[0]);
        }

        [Fact]
        public async Task MethodsWithNoMetadataAreNotInResults()
        {
            var p = Pattern.Rule(Pattern.Value('a'));
            var m = await Pattern.Match(p, "a");
            var md = m.End.Memos.GetMetadata(m.Value);
            Assert.Empty(md);
        }

        [Fact]
        public async Task ObjectsProducedByMultipleRulesDoNotGenerateExtraMetadata()
        {
            // The value produced should have all of the metadata for all of the 
            // matches that caused the production to suceed.
            var p = Pattern.Rule(
                Pattern.Production(
                    Pattern.And(
                        R1,
                        Pattern.Variable("v", R2),
                        R3),
                    (object v) => v));
            var m = await Pattern.Match(p, "abc");

            var md = m.End.Memos.GetMetadata(m.Value);
            Assert.Equal(3, md.Length);
        }


        [Fact]
        public async Task GetsRightMetadataWhenOrd()
        {
            var p = Pattern.Or(R1, R2, R3);
            var m = await Pattern.Match(p, "b");
            var metadata = m.End.Memos.GetMetadata(m.Value);

            Assert.Equal(1, metadata.Length);
            Assert.Equal(1, metadata[0].Metadata.Length);
            Assert.IsType<TestMetadata2>(metadata[0].Metadata[0]);
        }

		[Fact]
		public async Task GetsRightMetadataDuringDirectLeftRecursion()
		{
            var p = Pattern.Rule(
				Pattern.Or(
					Pattern.And(RecursionRule, R2),
					Pattern.And(RecursionRule, R3)));
            var m = await Pattern.Match(p, "aaac");

			var metadata = m.End.Memos.GetMetadata(m.Value);

			Assert.Equal(5, metadata.Length);
            var type = default(Type);
            var found = from md in metadata
                        from att in md.Metadata
                        where type.IsInstanceOfType(att)
                        select att;

            type = typeof(RecursionMetadata);
            Assert.Equal(1, found.Count());

            type = typeof(TestMetadata);
            Assert.Equal(3, found.Count());

            type = typeof(TestMetadata3);
            Assert.Equal(1, found.Count());
		}

        [Fact]
        public async Task MetadataComesFromCorrectOrigin()
        {
            var m = await Pattern.Match(R1, "a");
            var md = m.End.Memos.GetMetadata(m.Value);
            var a = md.Single();
            var o = a.Origin;
            Assert.Same(R1, o);
        }
        
        private static readonly Pattern R1 = Pattern.Rule(Pattern.Value('a'), new TestMetadata());
        private static readonly Pattern R2 = Pattern.Rule(Pattern.Value('b'), new TestMetadata2());
        private static readonly Pattern R3 = Pattern.Rule(Pattern.Value('c'), new TestMetadata3());
        private static readonly Pattern R4 = Pattern.Rule(Pattern.Any(), new TestMetadata4());

        // RecursionRule = RecursionRule R1 | R1;
        private static readonly Pattern RecursionRule;

        static MetadataTests()
        {
            RecursionRule = Pattern.Rule(
                Pattern.Or(
                    Pattern.And(
                        Pattern.Reference(() => RecursionRule),
                        R1),
                    R1),
                new RecursionMetadata());
		}
	}

	public class TestMetadata : MetadataAttribute
	{
	}
	public class TestMetadata2 : MetadataAttribute
	{
	}
	public class TestMetadata3 : MetadataAttribute
	{
	}
	public class TestMetadata4 : MetadataAttribute
	{
	}
	public class RecursionMetadata : MetadataAttribute
	{
	}
}